package com.javaio.demo.nio.server;


import java.io.*;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 服务端监听客户端发起建立连接要求
 */
public class ChatServer {

    private int SERVER_PROT = 8888;
    private final String TUICHU = "tuichu";

    /**
     * 缓冲区字节大小
     */
    private static final int BUFFER = 1024;

    /**
     * 同步阻塞服务端通道
     */
    private ServerSocketChannel serverSocketChannel;

    /**
     * 监控注册Channel(用来监控Channel 状态 是否是连接 或者读 或者取）
     */
    private Selector selector;
    /**
     * 缓冲区 因为nio通道Channle是往byteBuffer里面读取 bio是通过Steram流读取 这里采用的nio
     * ---处理读取的缓冲区
     */

    private ByteBuffer rBuffer = ByteBuffer.allocate(BUFFER);

    /**
     * 缓冲区 因为nio通道Channle是往byteBuffer里面读取 bio是通过Steram流读取 这里采用的nio
     * ---处理客户端连接和转发数据的缓冲区（SocketChannel）
     */
    private ByteBuffer wBuffer = ByteBuffer.allocate(BUFFER);

    /**
     * 编码解码 这里强制指定发送消息格式是UTF-8(避免乱码)
     */
    private Charset charset = Charset.forName("UTF-8");
    /**
     * 用来自定义用户自定义连接的端口
     */
    private int prot;


    /**
     * 用户自定义端口 不在使用固定的监听端口
     *
     * @param prot
     */
    public ChatServer(int prot) {
        //初始化赋值
        this.prot = prot;

    }

    /**
     * 用户自定义端口 如果用户没有自定义监听端口采用默认的8888
     */
    public ChatServer() {
        //初始化赋值
        this.prot = SERVER_PROT;

    }


    /**
     * 服务端主逻辑
     */
    public void start() {
        try {
            //打开通道用来进行连接
            serverSocketChannel = ServerSocketChannel.open();
            //通道可以阻塞但是nio模型要设置非阻塞configureBlocking 为false
            serverSocketChannel.configureBlocking(false);
            //使服务端通道监听端口
            serverSocketChannel.socket().bind(new InetSocketAddress(prot));
            //最重要环节 类似注册中心 打开注册中心
            selector = Selector.open();
            //将服务端通道注册到selector用来监听 避免数据混乱  第一个参数注册进去 第二个是监听事件  有读取和连接事件
            //监听ACCEPT阻塞连接状态 用来查看是否有用户连接
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            System.out.println("服务器启动端口：" + prot + " -->正在监听用户事件");
            //监听selector是否有事件返回
            while (true) {
                //阻塞 (如果有这个事件触发 才会返回如果没有就会阻塞)因为阻塞就要不停的循环查找是否有事件
                selector.select();

                //得到当前事件的key  key有一组 不只是一个  有可能发生在多个通道上
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                //如果有事件就要处理当前事件 -- 迭代处理
                for (SelectionKey selectionKey : selectionKeys) {
                    //处理当前事件
                    handles(selectionKey);
                }
                //如果处理完当前所以事件 要下一次重新处理新事件 也要从头开始(保证不能重复处理已经处理过的事件）
                selectionKeys.clear();
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //关闭 避免资源浪费
            close(selector);
        }
    }

    /**
     * 处理当前的selector事件(也就是处理服务端和客户端事件）
     *
     * @param key
     */
    public void handles(SelectionKey key) throws IOException {
        //如果触发了Accept事件 --客户端建立连接事件（服务端事件）
        if (key.isAcceptable()) {
            //得到服务器通道
            ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
            //处理客户端连接请求 和bio一样通过服务端accept获取客户端连接
            SocketChannel client = serverChannel.accept();
            //通道可以阻塞但是nio模型要设置非阻塞configureBlocking 为false
            client.configureBlocking(false);
            //将客户端管通道注册到selector  第二个参数是监听客户端读事件
            client.register(selector, SelectionKey.OP_READ);
            System.out.println("客户端" + client.socket().getPort() + "连接成功");
        }
        //如果是Read 事件 ---客户端发送消息事件（客户端事件）
        if (key.isReadable()) {
            //如果发现了读的事件获取当前事件的客户端
            SocketChannel client = (SocketChannel) key.channel();
            String fwdMsg = receive(client);
            //转发该客户端用户的消息
            if (fwdMsg.isEmpty()) {
                //如果没有消息那么不在监听该客户端了(因为和该客户端出现了异常），客户端异常处理
                //取消该通道
                key.cancel();
                //告诉selector  上面的问题所发生的变化
                selector.wakeup();
            } else {
                //没有异常 进行转发
                forwardMessage(client, fwdMsg);
                //检查用户是否退出,因为该用户退出连接了
                if (readyTuiChu(fwdMsg)) {
                    //取消该通道
                    key.cancel();
                    //告诉selector  上面的问题所发生的变化
                    selector.wakeup();
                    System.out.println("客户端" + client.socket().getPort() + "断开连接");
                }
            }

        }
    }

    /**
     * 当前用户消息进行转发
     *
     * @param selector
     * @param client
     * @param fwdMsg
     */
    private void forwardMessage(SocketChannel client, String fwdMsg) throws IOException {
        //selector.keys()  这里返回的是所有目前已经注册到selector上面的通道集合
        //selector.selectionKeys() 是返回所有有事件的通道集合
        for (SelectionKey selectionKey : selector.keys()) {
            Channel channelClient = selectionKey.channel();
            //这里要判断这些通道集合里面是否有服务端 有则跳出
            if (channelClient instanceof ServerSocketChannel) {
                continue;
            }
            //isValid() 判断是否是有效的key以及判断是否当前的key是否和当前用户一样
            if (selectionKey.isValid() && !Objects.equals(channelClient, client)) {
                //清空该缓冲区 避免有旧数据污染
                wBuffer.clear();
                //写数据put
                wBuffer.put(charset.encode("客户端：" + client.socket().getPort() + "" + fwdMsg));
               // 上面已经写入到了缓冲区这里要用函数flip进行读转换
                wBuffer.flip();
                //如果有残留数据也要写进通道
                while (wBuffer.hasRemaining()){
                    //强制转换
                    ((SocketChannel) channelClient).write(wBuffer);
                }
            }

        }
    }

    /**
     * 获取消息
     */
    public String receive(SocketChannel client) throws IOException {
        //清空该缓冲区 避免有旧数据污染
        rBuffer.clear();
        //因为是字节读取 要一个字节一个字节读取所以要循环  直到没有字节可读取退出  （这里要将客户端用户的数据循环写入到缓冲区）
        while (client.read(rBuffer) > 0) {
        }
        ;
        // 上面已经写入到了缓冲区这里要用函数flip进行读转换
        rBuffer.flip();
        //将字节转换
        return String.valueOf(charset.decode(rBuffer));


    }

    /**
     * 检查用户是否退出  退出放在服务端 是因为发起方是客户端
     */
    public boolean readyTuiChu(String msg) {
        //这里还要判断用户发送的是否是退出消息如果是则关闭它的消息连接
        return TUICHU.equals(msg) ? true : false;
    }

    /**
     * 关闭该服务端
     */
    public synchronized void close(Closeable closeable) {
        //关闭没用的socket的资源 避免占用资源过多消耗
        if (!Objects.equals(null, closeable)) {
            try {
                closeable.close();
                System.out.println("servereSocket资源已关闭");
            } catch (IOException e) {
                e.printStackTrace();
                System.out.println("该servereSocket资源关闭连接异常");
            }
        }
    }

    public static void main(String[] args) {
        ChatServer chatServer = new ChatServer();
        chatServer.start();
    }
}